﻿using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Xml;
using Co9320.NamaAlert.Engine.DebugHelper;
using Co9320.NamaAlert.Engine.Lists;
using Co9320.NamaAlert.Model;

namespace Co9320.NamaAlert.Engine.Feeder
{

	/// <summary>
	/// 生放送情報のFeedingクラス
	/// </summary>
	/// <remarks>
	/// 基本的に､ある時点において､このインスタンスの中に存在スレッドは単一である
	/// との仮定に基づいた設計｡
	/// 複数のスレッドが同一のタイミングでインスタンス中に存在する場合の結果は未定義｡
	/// </remarks>

	internal class NicoLiveLiveInfoFeeder
	{
		/// <summary>
		/// リセット後で0ページの時､それを最終ページ
		/// と誤検知させないためのフラグ｡
		/// </summary>
		private bool IsResetFirstFeeding_;


		/// <summary>
		/// ベースのURI｡パラメータ変化部は{0}で表現｡
		/// </summary>
		private string BaseUri_;

		public NicoLiveLiveInfoFeeder(string baseUri, string userAgent)
		{
			Debug.Assert(baseUri != null);
			Debug.Assert(baseUri.Trim().Length != 0);
			Debug.Assert(userAgent != null);
			Debug.Assert(userAgent.Trim().Length != 0);


			this.RecentLastPage = Define.InvalidValue;
			this.IsResetFirstFeeding_ = true;
			this.CurrentProgramCount = Define.InvalidValue;
			this.SizePerPage = Define.InvalidValue;

			this.BaseUri_ = baseUri;
			this.UserAgent = userAgent;


			try
			{
				var Ret = this.ReadPage();
				this.SizePerPage = Ret.Count;
			}
			catch (Exception ex)
			{
				LogOutputter.WriteElement(LogMode.Warning, ex);
			}

			this.ResetPage();
		}

		public int SizePerPage
		{
			get;
			private set;
		}

		public int CurrentProgramCount
		{
			get;
			private set;
		}


		public string BaseUri
		{
			get
			{
				return BaseUri_;
			}
		}

		public void ChangeURL(string newUriFormat)
		{
			if (newUriFormat == null || newUriFormat.Trim().Length == 0)
			{
				return;
			}

			this.RecentLastPage = Define.InvalidValue ;
			this.IsResetFirstFeeding_ = true;
			this.SizePerPage = Define.InvalidValue;

			this.BaseUri_ = newUriFormat;
			this.ResetPage();

			try
			{
				var Ret = this.ReadPage();
				this.SizePerPage = Ret.Count;
			}
			catch (Exception ex)
			{
				LogOutputter.WriteElement(LogMode.Warning, ex);
			}

			this.ResetPage();
		}

		public string UserAgent
		{
			get;
			private set;
		}


		public FeedingLivePrograms ReadPage()
		{
			FeedingLivePrograms Ret = null;

			DateTime HeaderTime = new DateTime();
			XmlDocument doc = null;

			WebResponse response = null;

			for (int i = 0; ; i++)
			{
				try
				{
					response = GetResponse();
					doc = BuildDocument(response);
					HeaderTime = GetHeaderTime(response);

					break;
				}
				catch (RssFeedingException)
				{
					throw;
				}
				catch (XmlException ex)
				{
					if (i < Define.ExternalRetryLimit)
					{
						System.Threading.Thread.Sleep(Define.InternalRetryDelay);

						LogOutputter.PushElement(LogMode.Warning);
						LogOutputter.WriteChildElement("Description", string.Format("XMLパースの解決に失敗しました.{0}回目のエラーです｡",i));
						LogOutputter.WriteChildElement("ExeptionMessage", ex.Message);
						LogOutputter.PopElement();

						continue;
					}
					else
					{
						throw new RssFeedingException("XMLパースエラーの結果発生するページスキップがリミットを超えました", ErrorLevel.Heavy, ex);
					}
				}
				catch (Exception ex)
				{
					if (i < Define.ExternalRetryLimit)
					{
						System.Threading.Thread.Sleep(Define.InternalRetryDelay);
						LogOutputter.WriteElement(LogMode.Warning,ex);

						continue;
					}
					else
					{
						throw new RssFeedingException(string.Format("リトライリミットの回数{0}を超えました", Define.ExternalRetryLimit), ErrorLevel.Intermediate,ex);
					}
				}
				finally
				{
					try
					{
						response.Close();
					}
					catch
					{
					}
				}
			}

			Ret = BuildList(doc, HeaderTime);

			if (this.CurrentPageCount == 1)
			{
				if (this.SizePerPage < Ret.Count)
				{
					this.SizePerPage = Ret.Count;
				}
			}


			if (Ret.Count == 0 && !IsResetFirstFeeding_)
			{
				RecentLastPage = CurrentPageCount - 1;
			}

			IsResetFirstFeeding_ = false;

			LogOutputter.OutputConsole("\t\tListCount:{0}", Ret.Count);


			return Ret;

		}

		private DateTime GetHeaderTime(WebResponse response)
		{
			DateTime GetTime = new DateTime();

			try
			{
				GetTime = DateTime.Parse(response.Headers.Get("Date"));
			}
			catch
			{
				GetTime = DateTime.Now;
			}
	
			return GetTime;
		}

		private HttpWebResponse GetResponse()
		{
			HttpWebResponse response = null;

			for (int i = 0; ; i++)
			{
				if (++CurrentPageCount >= Define.RssFeedLimit)
				{
					throw new RssFeedingException("読み込みのページリミットに達しました｡", ErrorLevel.Heavy);
				}


				HttpWebRequest req = (HttpWebRequest)WebRequest.Create(string.Format(BaseUri, CurrentPageCount));

				LogOutputter.OutputConsole("\tPageCount:{0}", CurrentPageCount);

				req.UserAgent = UserAgent;

				try
				{
					response = (HttpWebResponse)req.GetResponse();
					break;
				}
				catch
				{
					if (i < Define.ExternalRetryLimit)
					{
						LogOutputter.PushElement(LogMode.Warning);
						LogOutputter.WriteChildElement("Description", "HttpWebResponseの取得に失敗しました｡{0}回目のエラーです", i);
						LogOutputter.PopElement();
						System.Threading.Thread.Sleep(Define.InternalRetryDelay);
						continue;
					}
					else
					{
						throw new RssFeedingException("HttpWebResponseの取得に失敗しました", ErrorLevel.Heavy);
					}
				}
			}

			return response;
		}

		private XmlDocument BuildDocument(WebResponse response)
		{
			byte[] buff = new byte[1000];
			int size = 1;

			try
			{
				using (MemoryStream memstr = new MemoryStream())
				{
					using (Stream str = response.GetResponseStream())
					{
						while (size != 0)
						{
							size = str.Read(buff, 0, 1000);
							memstr.Write(buff, 0, size);
						}
					}

					return BuildDocument(memstr);
				}
			}
			catch (XmlException)
			{
				throw;
			}
			catch (Exception ex)
			{
				throw new RssFeedingException("WebResponseのResponseStreamを読み取り中に障害が発生しました｡", ErrorLevel.Intermediate, ex);
			}
		}



		private XmlDocument BuildDocument(Stream Str)
		{
			XmlDocument doc = new XmlDocument();
			Stream TargetStream = Str;
			TargetStream.Position = 0;


			for (int Retry = 0; ; Retry++)
			{
				try
				{
					doc.Load(TargetStream);
					break;
				}
				catch (XmlException ex)
				{
					LogOutputter.PushElement(LogMode.Warning);
					LogOutputter.WriteChildElement("Description", "{0}回目のXMLパースエラーが発生しました", Retry);
					LogOutputter.PopElement();

					if (Retry > Define.InternalRetryLimit)
					{
						throw ex;
					}

					TargetStream.Position = 0;

					MemoryStream AlternativeStream = new MemoryStream();
					StreamReader rdr = new StreamReader(TargetStream);
					StreamWriter wtr = new StreamWriter(AlternativeStream);

					for (int i = 1; ; i++)
					{
						string buff = rdr.ReadLine();

						if (buff == null)
						{
							break;
						}

						if (i == ex.LineNumber)
						{
							buff = buff.Remove(ex.LinePosition - 1, 1);
						}

						wtr.WriteLine(buff);
					}
					wtr.Flush();

					long Len = AlternativeStream.Length;

					rdr.Close();
					wtr.Close();

					TargetStream = new MemoryStream(AlternativeStream.GetBuffer(), 0,(int)Len, false);
					continue;
				}
			}

			return doc;
		}


		private FeedingLivePrograms BuildList(XmlDocument doc, DateTime GetTime)
		{
			Debug.Assert(doc != null);
			Debug.Assert(GetTime != DateTime.MinValue);
			Debug.Assert(GetTime != DateTime.MaxValue);

			FeedingLivePrograms Programs = null;


			XmlNamespaceManager nmgr = new XmlNamespaceManager(doc.NameTable);

			nmgr.AddNamespace("nico", "http://live.nicovideo.jp/");
			nmgr.AddNamespace("media", "http://search.yahoo.com/mrss/");


			XmlNodeList lst = doc.SelectNodes("/rss/channel/nico:total_count", nmgr);



			try
			{
				this.CurrentProgramCount = int.Parse(lst[0].InnerText);
				Programs = new FeedingLivePrograms(this.CurrentPageCount);
			}
			catch (FormatException)
			{

				this.CurrentPageCount = Define.InvalidValue;
				Programs = new FeedingLivePrograms(this.CurrentPageCount);
			}

			lst = doc.SelectNodes("/rss/channel/item");

			for (int i = 0; i < lst.Count; i++)
			{
				Programs.Add(BuildProgram(GetTime, lst[i], nmgr));
			}

			return Programs;
		}

		private LiveProgram BuildProgram(DateTime GetTime, XmlNode Node, XmlNamespaceManager nmgr)
		{
			Debug.Assert(Node != null);
			Debug.Assert(nmgr != null);

			LiveProgram program = new LiveProgram();
			Station station = program.Station;


			XmlNode tmp = Node.SelectSingleNode("title");
			Debug.Assert(tmp != null);
			program.Title = tmp.InnerText;

			tmp = Node.SelectSingleNode("pubDate");
			Debug.Assert(tmp != null);
			program.StartDateTime = DateTime.Parse(tmp.InnerText);

			tmp = Node.SelectSingleNode("guid");
			Debug.Assert(tmp != null);
			program.Id = int.Parse(tmp.InnerText.Trim('l', 'v'));

			tmp = Node.SelectSingleNode("description");
			Debug.Assert(tmp != null);
			program.ShortContent = tmp.InnerText;

			foreach (XmlNode node in Node.SelectNodes("category"))
			{
				if (node != null)
				{
					if (node.InnerText=="顔出し")
					{
						program.IsFace = true;
					}
					else if (node.InnerText == "凸待ち")
					{
						program.IsTotsu = true;
					}
					else
					{
						program.OfficialTag = node.InnerText;
					}
				}
			}

			tmp = Node.SelectSingleNode("nico:community_name", nmgr);
			Debug.Assert(tmp != null);
			station.Name = tmp.InnerText;

			tmp = Node.SelectSingleNode("nico:community_id", nmgr);
			Debug.Assert(tmp != null);
			station.Id = tmp.InnerText;

			tmp = Node.SelectSingleNode("nico:num_res", nmgr);
			Debug.Assert(tmp != null);
			program.CommentCount = int.Parse(tmp.InnerText);

			tmp = Node.SelectSingleNode("nico:view", nmgr);
			Debug.Assert(tmp != null);
			program.VisitorCount = int.Parse(tmp.InnerText);

			tmp = Node.SelectSingleNode("nico:member_only", nmgr);
			Debug.Assert(tmp != null);
			program.IsMemberOnly = bool.Parse(tmp.InnerText);

			tmp = Node.SelectSingleNode("nico:owner_name", nmgr);

			if (tmp != null)
			{
				program.CasterName = tmp.InnerText;
			}
			else
			{
				program.CasterName = null;
			}


			tmp = Node.SelectSingleNode("media:thumbnail", nmgr);
			program.Station.IconUri = new Uri(tmp.Attributes[0].InnerText);

			program.CrawlingDateTime = GetTime;


			return program;

		}


		public void ResetPage(int StartPage)
		{
			Debug.Assert(StartPage > 0);

			this.CurrentPageCount = StartPage - 1;
			this.IsResetFirstFeeding_ = true;
		}

		public void ResetPage()
		{
			ResetPage(1);
		}


		public int RecentLastPage
		{
			get;
			private set;
		}

		public int CurrentPageCount
		{
			get;
			private set;
		}
	}
}
